package top.ezzz.search;


import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.HttpException;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.Method;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONConfig;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import top.ezzz.search.exception.NoExistException;
import top.ezzz.search.model.*;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

/**
 * ElasticSearch 查询构建器
 *
 * @author noear
 * @since 1.0
 */
public class EsQuery {
    private final EsContext context;
    private final String indiceName;
    private final boolean isStream;

    private JSONObject dslq;
    private JSONObject queryMatch;
    private JSONObject item;


    private JSONConfig options;


    protected EsQuery(EsContext context, String indiceName, boolean isStream) {
        this.context = context;
        this.indiceName = indiceName;
        this.isStream = isStream;
        this.options = JSONConfig.create();
    }

    private HttpRequest getHttp(String path) {
        return context.getHttp(path);
    }

    private JSONObject getDslq() {
        if (dslq == null) {
            dslq = JSONUtil.createObj();
        }

        return dslq;
    }

    private JSONObject getQueryMatch() {
        if (queryMatch == null) {
            queryMatch = JSONUtil.createObj();
        }

        return queryMatch;
    }

    private String getJson(JSONObject obj) {
        return obj.toString();
    }

    public EsQuery options(JSONConfig options) {
        if (options != null) {
            this.options = options;
        }

        return this;
    }

    public EsQuery options(Consumer<JSONConfig> builder) {
        builder.accept(options);
        return this;
    }


    public EsQuery set(String field, Object value) {
        if (item == null) {
            item = JSONUtil.createObj();
        }

        item.set(field, value);
        return this;
    }

    ////////////////////////////////////////////////////////////////////////////////////////////////////

    //
    // insert
    //
    private String insertDo(JSONObject doc) throws HttpException {
        EsCommand cmd = new EsCommand();
        cmd.timeout = timeout;
        cmd.method = Method.POST.name();
        cmd.dslType = ContentType.JSON.getValue();
        cmd.dsl = getJson(doc);
        cmd.path = String.format("/%s/_doc/", indiceName);
        return context.execAsBody(cmd);
    }

    private String upsertDo(String docId, JSONObject doc) throws HttpException {
        EsCommand cmd = new EsCommand();
        cmd.timeout = timeout;
        cmd.method = Method.PUT.name();
        cmd.dslType = ContentType.JSON.getValue();
        doc.remove("_id"); //清理_id，否则source带_id报错
        cmd.dsl = getJson(doc);
        cmd.path = String.format("/%s/_doc/%s", indiceName, docId);
        return context.execAsBody(cmd);
    }

    /**
     * 插入
     */
    public String insert() throws HttpException {
        return insertDo(item);
    }

    public <T> String insert(T doc) throws HttpException {
        if (doc instanceof JSONObject) {
            return insertDo((JSONObject) doc);
        } else {
            return insertDo(JSONUtil.parseObj(doc, options));
        }
    }

    public <T> String insertList(List<T> docs) throws HttpException {
        StringBuilder docJson = new StringBuilder();
        String type = (isStream ? "create" : "index");
        docs.forEach((doc) -> {
            JSONObject object = JSONUtil.createObj().set(type, JSONUtil.createObj());
            docJson.append(getJson(object)).append("\n");
            if (doc instanceof JSONObject) {
                docJson.append(getJson((JSONObject) doc)).append("\n");
            } else {
                docJson.append(getJson(JSONUtil.parseObj(doc, options))).append("\n");
            }
        });
        EsCommand cmd = new EsCommand();
        cmd.timeout = timeout;
        cmd.method = Method.POST.name();
        cmd.dslType = ContentType.JSON.getValue();
        cmd.dsl = docJson.toString();
        if (context.getVersion() > Constants.Es7) {
            cmd.path = String.format("/%s/_bulk", indiceName); //"/_bulk";
        } else {
            cmd.path = String.format("/%s/_doc/_bulk", indiceName);
        }
        return context.execAsBody(cmd);
    }


    public String upsert(String docId) throws HttpException {
        return upsertDo(docId, item);
    }

    public <T> String upsert(String docId, T doc) throws HttpException {
        if (doc instanceof JSONObject) {
            return upsertDo(docId, (JSONObject) doc);
        } else {
            return upsertDo(docId, JSONUtil.parseObj(doc, options));
        }
    }

    public <T> String upsertList(Map<String, T> docs) throws HttpException {
        StringBuilder docJson = new StringBuilder();
        String type = (isStream ? "create" : "index");
        docs.forEach((docId, doc) -> {
            JSONObject object = JSONUtil.createObj().set(type, JSONUtil.createObj().set("_id", docId));
            docJson.append(getJson(object)).append("\n");
            if (doc instanceof JSONObject) {
                docJson.append(getJson((JSONObject) doc)).append("\n");
            } else {
                docJson.append(getJson(JSONUtil.parseObj(doc, options))).append("\n");
            }
        });
        EsCommand cmd = new EsCommand();
        cmd.timeout = timeout;
        cmd.method = Method.POST.name();
        cmd.dslType = ContentType.JSON.getValue();
        cmd.dsl = docJson.toString();
        if (context.getVersion() > Constants.Es7) {
            cmd.path = String.format("/%s/_bulk", indiceName);//cmd.path = "/_bulk";
        } else {
            cmd.path = String.format("/%s/_doc/_bulk", indiceName);
        }
        return context.execAsBody(cmd);
    }


    //
    // select
    //
    public EsQuery where(Consumer<EsCondition> condition) {
        JSONObject obj = JSONUtil.createObj();
        EsCondition c = new EsCondition(obj);
        condition.accept(c);
        if (obj.isEmpty()) {
            return this;
        }
        getDslq().set("query", obj);
        return this;
    }

    private String _format;

    public EsQuery sql(String sql, String format) {
        _format = format;
        getDslq().set("query", sql);
        return this;
    }

    public EsQuery sql(String sql) {
        return sql(sql, "json");
    }


    private static final int limit_max_hits = 10000;

    public EsQuery limit(int start, int size) {
        getDslq().set("from", start);
        getDslq().set("size", size);
        if (size >= limit_max_hits || (start + size) >= limit_max_hits) {
            getDslq().set("track_total_hits", "true");
        }
        return this;
    }

    public EsQuery trackTotalHits() {
        getDslq().set("track_total_hits", "true");
        return this;
    }

    public EsQuery limit(int size) {
        getDslq().set("size", size);
        if (size >= limit_max_hits) {
            getDslq().set("track_total_hits", "true");
        }
        return this;
    }

    //
    //排序
    //

    public EsQuery orderBy(Consumer<EsSort> sort) {
        if (getDslq().get("sort") == null) {
            getDslq().set("sort", JSONUtil.createArray());
        }
        EsSort s = new EsSort(getDslq().getJSONArray("sort"));
        sort.accept(s);
        return this;
    }

    public EsQuery orderByAsc(String field) {
        if (getDslq().get("sort") == null) {
            getDslq().set("sort", JSONUtil.createObj());
        }
        JSONObject sort = getDslq().getJSONObject("sort");
        if (sort.get(field) == null) {
            sort.set(field, JSONUtil.createObj());
        }
        sort.getJSONObject(field).set("order", "asc");
        return this;
    }

    public EsQuery orderByDesc(String field) {
        if (getDslq().get("sort") == null) {
            getDslq().set("sort", JSONUtil.createObj());
        }
        JSONObject sort = getDslq().getJSONObject("sort");
        if (sort.get(field) == null) {
            sort.set(field, JSONUtil.createObj());
        }
        sort.getJSONObject(field).set("order", "desc");
        return this;
    }

    public EsQuery andByAsc(String field) {
        if (getDslq().get("sort") == null) {
            getDslq().set("sort", JSONUtil.createObj());
        }
        JSONObject sort = getDslq().getJSONObject("sort");
        if (sort.get(field) == null) {
            sort.set(field, JSONUtil.createObj());
        }
        sort.getJSONObject(field).set("order", "asc");
        return this;
    }

    public EsQuery andByDesc(String field) {
        if (getDslq().get("sort") == null) {
            getDslq().set("sort", JSONUtil.createObj());
        }
        JSONObject sort = getDslq().getJSONObject("sort");
        if (sort.get(field) == null) {
            sort.set(field, JSONUtil.createObj());
        }
        sort.getJSONObject(field).set("order", "desc");
        return this;
    }

    /**
     * search_after
     */
    public EsQuery onAfter(Object... values) {
        if (getDslq().get("search_after") == null) {
            getDslq().set("search_after", JSONUtil.createArray());
        }
        getDslq().getJSONArray("search_after").addAll(Arrays.asList(values));
        return this;
    }

    /**
     * min_score
     */
    public EsQuery minScore(Object value) {
        if (getDslq().get("min_score") == null) {
            getDslq().set("min_score", null);
        }
        getDslq().set("min_score", value);
        return this;
    }

    //
    //aggs
    //
    public EsQuery aggs(Consumer<EsAggs> aggs) {
        if (getDslq().get("aggs") == null) {
            getDslq().set("aggs", JSONUtil.createObj());
        }
        EsAggs a = new EsAggs(getDslq().getJSONObject("aggs"));
        aggs.accept(a);
        return this;
    }

    //
    //highlight
    //
    public EsQuery highlight(Consumer<EsHighlight> highlight) {
        if (getDslq().get("highlight") == null) {
            getDslq().set("highlight", JSONUtil.createObj());
        }
        EsHighlight h = new EsHighlight(getDslq().getJSONObject("highlight"));
        highlight.accept(h);
        return this;
    }

    int timeout;

    public EsQuery timeout(int timeoutSeconds) {
        if (timeoutSeconds > 0) {
            timeout = timeoutSeconds;
        }

        return this;
    }

    //
    // select
    //
    public String select(String dsl) throws HttpException {
        EsCommand cmd = new EsCommand();
        cmd.timeout = timeout;
        cmd.method = Method.POST.name();
        cmd.dslType = ContentType.JSON.getValue();
        cmd.dsl = dsl;
        if (StrUtil.isEmpty(_format)) {
            cmd.path = String.format("/%s/_search", indiceName);
        } else {
            cmd.path = String.format("/_sql?format=%s", _format);
        }
        return context.execAsBody(cmd);
    }


    public String selectJson() throws HttpException {
        return selectJson(null);
    }

    public String selectJson(String fields) throws HttpException {
        if (StrUtil.isNotEmpty(fields)) {
            if (getDslq().get("_source") == null) {
                getDslq().set("_source", JSONUtil.createObj());
            }
            EsSource s = new EsSource(getDslq().getJSONObject("_source"));
            if (fields.startsWith("!")) {
                s.excludes(fields.substring(1).split(","));
            } else {
                s.includes(fields.split(","));
            }
        }
        return select(getJson(getDslq()));
    }

    public JSONObject selectNode() throws HttpException {
        return JSONUtil.parseObj(selectJson());
    }

    public JSONObject selectNode(String fields) throws HttpException {
        return JSONUtil.parseObj(selectJson(fields));
    }

    public JSONObject selectAggs() throws HttpException {
        return selectNode().get("aggregations", JSONObject.class, true);
    }

    public JSONObject selectAggs(String fields) throws HttpException {
        return selectNode(fields).get("aggregations", JSONObject.class, true);
    }


    public Map selectMap() throws HttpException {
        return selectOne(Map.class);
    }

    public Map selectMap(String fields) throws HttpException {
        return selectOne(Map.class, fields);
    }

    public List<Map> selectMapList() throws HttpException {
        return selectList(Map.class).getList();
    }

    public List<Map> selectMapList(String fields) throws HttpException {
        return selectList(Map.class, fields).getList();
    }

    public <T> T selectOne(Class<T> clz) throws HttpException {
        return selectOne(clz, null);
    }

    public <T> T selectOne(Class<T> clz, String fields) throws HttpException {
        limit(1);
        EsData<T> page = selectList(clz, fields);
        if (page.getListSize() > 0) {
            return page.getList().get(0);
        } else {
            return null;
        }
    }

    public <T> EsData<T> selectList(Class<T> clz) throws HttpException {
        return selectList(clz, null);
    }

    public <T> EsData<T> selectList(Class<T> clz, String fields) throws HttpException {
        if (queryMatch != null) {
            if (getDslq().get("query") == null) {
                getDslq().set("query", JSONUtil.createObj());
            }
            if (queryMatch.size() > 1) {
                getDslq().getJSONObject("query").set("multi_match", queryMatch);
            } else {
                getDslq().getJSONObject("query").set("match", queryMatch);
            }
        }
        String json = selectJson(fields);
        JSONObject oHits = JSONUtil.parseObj(json).getJSONObject("hits");
        oHits = oHits == null ? new JSONObject() : oHits;
        long total = 0;
        //低版本es返回的json中total是一个数，而不是一个对象
        if (context.getVersion() > 6) {
            total = Convert.toLong(oHits.getByPath("total.value"), 0L);
        } else {
            total = oHits.getLong("total", 0L);
        }
        double max_score = oHits.getDouble("max_score", 0.0);
        JSONArray hits = oHits.getJSONArray("hits");
        hits = hits == null ? new JSONArray() : hits;
        hits.forEach(item -> {
            JSONObject one = (JSONObject) item;
            one.set("_id", one.get("_id"));
            one.set("_score", one.get("_score"));
            one.putAll(one.get("_source", JSONObject.class));
        });
        List<T> list = oHits.getBeanList("hits", clz);
        return new EsData<>(total, max_score, list);
    }


    //
    // selectByIds
    //
    public <T> List<T> selectByIds(Class<T> clz, List<String> docIds) throws HttpException {
        try {
            JSONObject obj = JSONUtil.parseObj(" {\"query\":{\"ids\":{\"values\":[]}}}");
            obj.getByPath("query.ids.values", JSONArray.class).addAll(docIds);
            String json = select(getJson(obj));
            JSONObject oHits = JSONUtil.parseObj(json).getJSONObject("hits");
            oHits.get("hits", JSONArray.class).forEach(item -> {
                JSONObject one = (JSONObject) item;
                one.putAll(one.getJSONObject("_source"));
            });
            return oHits.getBeanList("hits", clz);
        } catch (NoExistException e) {
            return null;
        }
    }

    //
    // selectById
    //
    public <T> T selectById(Class<T> clz, String docId) throws HttpException {
        try {
            EsCommand cmd = new EsCommand();
            cmd.timeout = timeout;
            cmd.method = Method.GET.name();
            cmd.path = String.format("/%s/_doc/%s", indiceName, docId);
            String body = context.execAsBody(cmd);
            JSONObject oItem = JSONUtil.parseObj(body);
            JSONObject source = oItem.getJSONObject("_source");
            if (source == null) {
                oItem = null;
            } else {
                oItem.putAll(source);
            }
            return JSONUtil.toBean(oItem, clz);
        } catch (NoExistException e) {
            return null;
        }
    }


    /**
     * delete
     *
     * @return
     * @throws HttpException
     */
    public String delete() throws HttpException {
        if (queryMatch != null) {
            if (getDslq().get("query") == null) {
                getDslq().set("query", JSONUtil.createObj());
            }
            if (queryMatch.size() > 1) {
                getDslq().getJSONObject("query").set("multi_match", queryMatch);
            } else {
                getDslq().getJSONObject("query").set("match", queryMatch);
            }
        }
        EsCommand cmd = new EsCommand();
        cmd.timeout = timeout;
        cmd.method = Method.POST.name();
        cmd.dslType = ContentType.JSON.getValue();
        cmd.dsl = getJson(getDslq());
        cmd.path = String.format("/%s/_delete_by_query", indiceName);
        return context.execAsBody(cmd);
    }


    public boolean deleteById(String docId) throws HttpException {
        EsCommand cmd = new EsCommand();
        cmd.timeout = timeout;
        cmd.method = Method.DELETE.name();
        cmd.path = String.format("/%s/_doc/%s", indiceName, docId);
        try {
            context.execAsBody(cmd);
            return true;
        } catch (NoExistException e) {
            return true;
        }
    }
}
